/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.rpc.http;

import org.openislands.oi.config.argumentresolver.EnumPathVariable;
import org.openislands.oi.config.argumentresolver.EnumVariable;
import org.openislands.oi.rpc.base.Executor;
import org.openislands.oi.rpc.base.Extended;
import org.openislands.oi.rpc.base.ItemExtended;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@SuppressWarnings("unused")
@Component
public class RestControllerInterceptor extends HttpRequestInterceptor {
    private static final Logger log = LoggerFactory.getLogger(RestControllerInterceptor.class);

    /***
     * RequestMapping request method dict
     * {@link Integer}          RequestMapping.class
     * {@link RequestMethod}    request method
     */
    private final Map<Class<?>, RequestMethod> mappingMethod = new HashMap<>();

    /***
     * Intercept method info cache
     * Method signature, {@link MethodCacheInfo}. Load in {@link #getMethodCache(Class, Method, Object[])}
     */
    private final Map<String, MethodCacheInfo> methodCacheInfoCache = new ConcurrentHashMap<>();

    @Autowired
    private RequestHandlerContext requestHandlerContext;

    public RestControllerInterceptor() {
        this.initHandler();
    }

    public void initHandler() {
        this.mappingMethod.put(RequestMapping.class, RequestMethod.OPTIONS);
        this.mappingMethod.put(GetMapping.class, RequestMethod.GET);
        this.mappingMethod.put(PostMapping.class, RequestMethod.POST);
        this.mappingMethod.put(PutMapping.class, RequestMethod.PUT);
        this.mappingMethod.put(DeleteMapping.class, RequestMethod.DELETE);
        this.mappingMethod.put(PatchMapping.class, RequestMethod.PATCH);
    }

    @Override
    public Object intercept(Object proxyObject, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        final Class<?> superclass = proxyObject.getClass().getSuperclass();
        end:
        {
            // check controller
            if (method.getDeclaringClass() == Object.class || !(superclass.isAnnotationPresent(Controller.class) ||
                    superclass.isAnnotationPresent(RestController.class))) {
                break end;
            }

            // get cache
            MethodCacheInfo cacheInfo = getMethodCache(superclass, method, args);
            if (cacheInfo == MethodCacheInfo.EMPTY) break end;
            RequestMethod requestMethod = cacheInfo.getRequestMethod();
            MethodVariable methodVariable = cacheInfo.parseMethod(method, args);
            Object requestBody = methodVariable.getRequestBody();
            Map<String, Object> uriVariables = methodVariable.getUriVariables();

            // add http extended
            final Executor<?> executor = this.getExecutor();
            Extended<Class<?>, ItemExtended> extended = executor.extended();
            ItemExtended httpExtended = extended.get(HttpExtended.class, new HttpExtended());

            httpExtended.putItem(HttpExtended.URL_PATH, cacheInfo.getClassPath() + cacheInfo.getMethodPath());
            httpExtended.putItem(HttpExtended.URI_VARIABLE, uriVariables);
            httpExtended.putItem(HttpExtended.REQUEST_BODY, requestBody);
            httpExtended.putItem(HttpExtended.RESPONSE_CLASS, method.getReturnType());
            log.info("request uriVariables: {} requestMethod: {}, requestBody: {}", uriVariables, requestMethod, requestBody);
            // send request
            return this.requestHandlerContext.request(requestMethod, executor);
        }
        return methodProxy.invokeSuper(proxyObject, args);
    }

    public static RequestMethod requestMappingMethod(Annotation annotation) {
        if (annotation instanceof RequestMapping) {
            final RequestMethod[] method = ((RequestMapping) annotation).method();
            return method.length > 0 ? method[0] : null;
        }

        final RequestMapping requestMapping = annotation.annotationType().getAnnotation(RequestMapping.class);
        if (Objects.nonNull(requestMapping)) {
            return requestMappingMethod(requestMapping);
        }
        return null;
    }

    public static String requestMappingPath(Annotation requestMapping) {
        String path = "/";
        if (Objects.nonNull(requestMapping)) {
            try {
                final Class<? extends Annotation> aClass = requestMapping.annotationType();
                final Method pathMethod = aClass.getMethod("path");
                String[] strings = ((String[]) pathMethod.invoke(requestMapping));
                if (strings.length == 0) {
                    final Method valueMethod = aClass.getMethod("value");
                    strings = ((String[]) valueMethod.invoke(requestMapping));
                }
                if (strings.length > 0 && !(path = strings[0]).startsWith("/")) {
                    path = "/" + path;
                }
            } catch (Exception ignored) {
            }
        }
        return path;
    }

    public MethodCacheInfo getMethodCache(Class<?> clazz, Method method, Object[] args) {
        String methodSignature = method.toString();
        MethodCacheInfo methodCacheInfo = methodCacheInfoCache.get(methodSignature);
        if (methodCacheInfo == null) {
            // get class path
            RequestMapping classMapping = clazz.getAnnotation(RequestMapping.class);
            String classPath = requestMappingPath(classMapping);
            // get method path and type
            String methodPath = "";
            RequestMethod requestMethod = null;
            final Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                if (Objects.nonNull(requestMethod = this.mappingMethod.get(annotation.annotationType()))) {
                    if (requestMethod == RequestMethod.OPTIONS) {
                        requestMethod = requestMappingMethod(annotation);
                    }

                    methodPath = requestMappingPath(annotation);
                    break;
                }
            }

            // method type not specified
            if (Objects.isNull(requestMethod)) {
                methodCacheInfoCache.put(methodSignature, MethodCacheInfo.EMPTY);
                return MethodCacheInfo.EMPTY;
            }

            // set parameter parser
            methodCacheInfo = new MethodCacheInfo(classPath, methodPath, requestMethod);
            final Parameter[] parameters = method.getParameters();
            for (Parameter parameter : parameters) {
                methodCacheInfo.getParserList().add(ParameterParser.parser(parameter));
            }
            methodCacheInfoCache.put(methodSignature, methodCacheInfo);
        }

        return methodCacheInfo;
    }

    private static final class MethodVariable {
        private Object requestBody;
        private final Map<String, Object> uriVariables = new HashMap<>();

        public void setRequestBody(Object requestBody) {
            this.requestBody = requestBody;
        }

        public Object getRequestBody() {
            return requestBody;
        }

        public Map<String, Object> getUriVariables() {
            return uriVariables;
        }
    }

    private static final class MethodCacheInfo {
        public static final MethodCacheInfo EMPTY = new MethodCacheInfo("", "", null);
        private final List<ParameterParser> parserList = new ArrayList<>();
        private final RequestMethod requestMethod;
        private final String classPath;
        private final String methodPath;

        public MethodCacheInfo(String classPath, String methodPath, RequestMethod requestMethod) {
            this.classPath = classPath;
            this.methodPath = methodPath;
            this.requestMethod = requestMethod;
        }

        public List<ParameterParser> getParserList() {
            return parserList;
        }

        public String getClassPath() {
            return classPath;
        }

        public String getMethodPath() {
            return methodPath;
        }

        public RequestMethod getRequestMethod() {
            return requestMethod;
        }

        public MethodVariable parseMethod(Method method, Object[] args) {
            MethodVariable methodVariable = new MethodVariable();
            final Parameter[] parameters = method.getParameters();
            for (int index = 0; index < parameters.length; index++) {
                parserList.get(index).parse(methodVariable,
                        parameters[index], args.length > index ? args[index] : null);
            }
            return methodVariable;
        }
    }

    interface ParameterParser {
        void parse(MethodVariable methodVariable, Parameter parameter, Object arg);

        static ParameterParser parser(Parameter parameter) {
            if (parameter.isAnnotationPresent(RequestBody.class)) {
                return (methodVariable, parameter1, arg) -> methodVariable.setRequestBody(arg);
            } else if (parameter.isAnnotationPresent(PathVariable.class)) {
                return (methodVariable, parameter12, arg) -> {
                    PathVariable pathVariable = parameter12.getAnnotation(PathVariable.class);
                    String name = pathVariable.name();
                    methodVariable.getUriVariables().put(name.trim().equals("") ? pathVariable.value() : name, arg);
                };
            } else if (parameter.isAnnotationPresent(EnumPathVariable.class)) {
                return (methodVariable, parameter13, arg) -> {
                    EnumPathVariable pathVariable = parameter13.getAnnotation(EnumPathVariable.class);
                    String var = pathVariable.name().isEmpty() ? pathVariable.value() : pathVariable.name();

                    Class<?> parameterType;
                    EnumVariable enumVariable = parameter13.getType().getAnnotation(EnumVariable.class);
                    if (enumVariable != null && arg != null && arg.getClass() == (parameterType = parameter13.getType())) {
                        String field = enumVariable.value().isEmpty() ? enumVariable.field() : enumVariable.value();
                        try {
                            Field declaredField = parameterType.getDeclaredField(field);
                            declaredField.setAccessible(true);
                            methodVariable.getUriVariables().put(var, declaredField.get(arg));
                        } catch (ReflectiveOperationException ignored) {
                        }
                        return;
                    }
                    methodVariable.getUriVariables().put(var, arg);
                };
            }

            // empty implements
            return (methodVariable, parameter13, arg) -> {
            };
        }
    }
}
